﻿Ext.util.MixedCollection = function (allowFunctions, keyFn) {
    this.items = [];
    this.map = {};
    this.keys = [];
    this.length = 0;
    this.addEvents(

        'clear',

        'add',

        'replace',

        'remove',
        'sort'
    );
    //是否允许函数类型的元素，默认为不允许（false）
    this.allowFunctions = allowFunctions === true;
    //keyFn：对于一个在该Mixed集合中已保存类型的item，可用这个函数返回对应item的键值。
    //执行MixedCollection方法时不需要特定传入一个key参数便可查找出键值。
    //传入这个参数等同于实现了getKey方法。 
    if (keyFn) {
        this.getKey = keyFn;
    }
    Ext.util.MixedCollection.superclass.constructor.call(this);
};

Ext.extend(Ext.util.MixedCollection, Ext.util.Observable, {


    allowFunctions: false,

    //加入一个item到集合中。
    //key：item的键名称
    //obj：加入的item 
    add: function (key, obj) {
        var myObj = obj, myKey = key;
        if (arguments.length == 1) {//如果只有key参数，则myKey: obj.id, myObj: obj
            myObj = myKey;
            myKey = this.getKey(myObj);//如果从构造函数传入了自定义的getKey，则用自定义的
        }
        if (typeof myKey != 'undefined' && myKey !== null) {
            var old = this.map[myKey];
            if (typeof old != 'undefined') {//如果map对象中已经存在属性名为myKey值的属性
                //替换集合中的item。完成后触发replace事件。 在replace中，items和keys数组中的元素也随之改变
                return this.replace(myKey, myObj); 
            }
            this.map[myKey] = myObj;
        }
        this.length++;
        this.items.push(myObj);//将myObj放入items数组中
        this.keys.push(myKey);//将myKey放入keys数组中
        this.fireEvent('add', this.length - 1, myObj, myKey);//触发add事件
        return myObj;
    },

    //根据item找到key 
    getKey: function (o) {
        return o.id;
    },

    //替换集合中的item。完成后触发replace事件。 
    replace: function (key, o) {
        if (arguments.length == 1) {//如果只有key参数，则key: o.id, o: o
            o = arguments[0];
            key = this.getKey(o);
        }
        var old = this.map[key];
        if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
            return this.add(key, o);//如果没有冲突则采用add方法
        }
        var index = this.indexOfKey(key); //传入一个Key，返回它的索引。 
        this.items[index] = o;
        this.map[key] = o;
        this.fireEvent('replace', key, old, o);//触发replace事件
        return o;
    },

    //将数组中或是对象中的所有元素加入到集合中。 
    addAll: function (objs) {
        if (arguments.length > 1 || Ext.isArray(objs)) {//如果是多个参数或数组
            var args = arguments.length > 1 ? arguments : objs;
            for (var i = 0, len = args.length; i < len; i++) {
                this.add(args[i]);
            }
        } else {//如果是对象
            for (var key in objs) {
                if (!objs.hasOwnProperty(key)) {
                    continue;
                }
                if (this.allowFunctions || typeof objs[key] != 'function') {
                    this.add(key, objs[key]);
                }
            }
        }
    },

    //在集合中执行每个Item的指定函数。
    each: function (fn, scope) {
        var items = [].concat(this.items);
        for (var i = 0, len = items.length; i < len; i++) {
            if (fn.call(scope || items[i], items[i], i, len) === false) {
                break;
            }
        }
    },

    //传入一个函数类型的参数，然后在集合中的每个Item执行
    eachKey: function (fn, scope) {
        for (var i = 0, len = this.keys.length; i < len; i++) {
            fn.call(scope || window, this.keys[i], this.items[i], i, len);
        }
    },

    //根据传入的函数,执行该函数,若返回true便说明这是要找到的那个item。 
    findBy: function (fn, scope) {
        for (var i = 0, len = this.items.length; i < len; i++) {
            if (fn.call(scope || window, this.items[i], this.keys[i])) {
                return this.items[i];
            }
        }
        return null;
    },



    insert: function (index, key, obj) {
        var myKey = key, myObj = obj;
        if (arguments.length == 2) {
            myObj = myKey;
            myKey = this.getKey(myObj);
        }
        if (this.containsKey(myKey)) {
            this.suspendEvents();
            this.removeByKey(myKey);
            this.resumeEvents();
        }
        if (index >= this.length) {
            return this.add(myKey, myObj);
        }
        this.length++;
        this.items.splice(index, 0, myObj);
        if (typeof myKey != 'undefined' && myKey !== null) {
            this.map[myKey] = myObj;
        }
        this.keys.splice(index, 0, myKey);
        this.fireEvent('add', index, myObj, myKey);
        return myObj;
    },


    remove: function (o) {
        return this.removeAt(this.indexOf(o));
    },


    removeAll: function (items) {
        Ext.each(items || [], function (item) {
            this.remove(item);
        }, this);

        return this;
    },


    removeAt: function (index) {
        if (index < this.length && index >= 0) {
            this.length--;
            var o = this.items[index];
            this.items.splice(index, 1);
            var key = this.keys[index];
            if (typeof key != 'undefined') {
                delete this.map[key];
            }
            this.keys.splice(index, 1);
            this.fireEvent('remove', o, key);
            return o;
        }
        return false;
    },


    removeByKey: function (key) {
        return this.removeAt(this.indexOfKey(key));
    },

    removeKey: function () {
        console.warn('MixedCollection: removeKey has been deprecated. Please use removeByKey.');
        return this.removeByKey.apply(this, arguments);
    },


    getCount: function () {
        return this.length;
    },


    indexOf: function (o) {
        return this.items.indexOf(o);
    },

    //传入一个Key，返回它的索引。 
    indexOfKey: function (key) {
        return this.keys.indexOf(key);
    },


    get: function (key) {
        var mk = this.map[key],
            item = mk !== undefined ? mk : (typeof key == 'number') ? this.items[key] : undefined;
        return typeof item != 'function' || this.allowFunctions ? item : null;
    },

    item: function () {
        console.warn('MixedCollection: item has been deprecated. Please use get.');
        return this.get.apply(this, arguments);
    },


    getAt: function (index) {
        return this.items[index];
    },

    itemAt: function () {
        console.warn('MixedCollection: itemAt has been deprecated. Please use getAt.');
        return this.getAt.apply(this, arguments);
    },


    getByKey: function (key) {
        return this.map[key];
    },

    key: function () {
        console.warn('MixedCollection: key has been deprecated. Please use getByKey.');
        return this.getByKey.apply(this, arguments);
    },


    contains: function (o) {
        return this.indexOf(o) != -1;
    },


    containsKey: function (key) {
        return typeof this.map[key] != 'undefined';
    },


    clear: function () {
        this.length = 0;
        this.items = [];
        this.keys = [];
        this.map = {};
        this.fireEvent('clear');
    },


    first: function () {
        return this.items[0];
    },


    last: function () {
        return this.items[this.length - 1];
    },


    _sort: function (property, dir, fn) {
        var i, len,
            dsc = String(dir).toUpperCase() == 'DESC' ? -1 : 1,


            c = [],
            keys = this.keys,
            items = this.items;


        fn = fn || function (a, b) {
            return a - b;
        };


        for (i = 0, len = items.length; i < len; i++) {
            c[c.length] = {
                key: keys[i],
                value: items[i],
                index: i
            };
        }


        c.sort(function (a, b) {
            var v = fn(a[property], b[property]) * dsc;
            if (v === 0) {
                v = (a.index < b.index ? -1 : 1);
            }
            return v;
        });


        for (i = 0, len = c.length; i < len; i++) {
            items[i] = c[i].value;
            keys[i] = c[i].key;
        }

        this.fireEvent('sort', this);
    },


    sort: function (property, direction) {

        var sorters = property;


        if (Ext.isString(property)) {
            sorters = [new Ext.util.Sorter({
                property: property,
                direction: direction || "ASC"
            })];
        } else if (property instanceof Ext.util.Sorter) {
            sorters = [property];
        } else if (Ext.isObject(property)) {
            sorters = [new Ext.util.Sorter(property)];
        }

        var length = sorters.length;

        if (length == 0) {
            return;
        }


        var sorterFn = function (r1, r2) {
            var result = sorters[0].sort(r1, r2),
                length = sorters.length,
                i;


            for (i = 1; i < length; i++) {
                result = result || sorters[i].sort.call(this, r1, r2);
            }

            return result;
        };

        this.sortBy(sorterFn);
    },


    sortBy: function (sorterFn) {
        var items = this.items,
            keys = this.keys,
            length = items.length,
            temp = [],
            i;


        for (i = 0; i < length; i++) {
            temp[i] = {
                key: keys[i],
                value: items[i],
                index: i
            };
        }

        temp.sort(function (a, b) {
            var v = sorterFn(a.value, b.value);
            if (v === 0) {
                v = (a.index < b.index ? -1 : 1);
            }

            return v;
        });


        for (i = 0; i < length; i++) {
            items[i] = temp[i].value;
            keys[i] = temp[i].key;
        }

        this.fireEvent('sort', this);
    },


    reorder: function (mapping) {
        this.suspendEvents();

        var items = this.items,
            index = 0,
            length = items.length,
            order = [],
            remaining = [],
            oldIndex;


        for (oldIndex in mapping) {
            order[mapping[oldIndex]] = items[oldIndex];
        }

        for (index = 0; index < length; index++) {
            if (mapping[index] == undefined) {
                remaining.push(items[index]);
            }
        }

        for (index = 0; index < length; index++) {
            if (order[index] == undefined) {
                order[index] = remaining.shift();
            }
        }

        this.clear();
        this.addAll(order);

        this.resumeEvents();
        this.fireEvent('sort', this);
    },


    sortByKey: function (dir, fn) {
        this._sort('key', dir, fn || function (a, b) {
            var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase();
            return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
        });
    },

    keySort: function () {
        console.warn('MixedCollection: keySort has been deprecated. Please use sortByKey.');
        return this.sortByKey.apply(this, arguments);
    },



    getRange: function (start, end) {
        var items = this.items;
        if (items.length < 1) {
            return [];
        }
        start = start || 0;
        end = Math.min(typeof end == 'undefined' ? this.length - 1 : end, this.length - 1);
        var i, r = [];
        if (start <= end) {
            for (i = start; i <= end; i++) {
                r[r.length] = items[i];
            }
        } else {
            for (i = start; i >= end; i--) {
                r[r.length] = items[i];
            }
        }
        return r;
    },


    filter: function (property, value, anyMatch, caseSensitive) {
        var filters = [];


        if (Ext.isString(property)) {
            filters.push(new Ext.util.Filter({
                property: property,
                value: value,
                anyMatch: anyMatch,
                caseSensitive: caseSensitive
            }));
        } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
            filters = filters.concat(property);
        }



        var filterFn = function (record) {
            var isMatch = true,
                length = filters.length,
                i;

            for (i = 0; i < length; i++) {
                var filter = filters[i],
                    fn = filter.filterFn,
                    scope = filter.scope;

                isMatch = isMatch && fn.call(scope, record);
            }

            return isMatch;
        };

        return this.filterBy(filterFn);
    },


    filterBy: function (fn, scope) {
        var newMC = new Ext.util.MixedCollection(),
            keys = this.keys,
            items = this.items,
            length = items.length,
            i;

        newMC.getKey = this.getKey;

        for (i = 0; i < length; i++) {
            if (fn.call(scope || this, items[i], keys[i])) {
                newMC.add(keys[i], items[i]);
            }
        }

        return newMC;
    },


    findIndex: function (property, value, start, anyMatch, caseSensitive) {
        if (Ext.isEmpty(value, false)) {
            return -1;
        }
        value = this.createValueMatcher(value, anyMatch, caseSensitive);
        return this.findIndexBy(function (o) {
            return o && value.test(o[property]);
        }, null, start);
    },


    findIndexBy: function (fn, scope, start) {
        var k = this.keys, it = this.items;
        for (var i = (start || 0), len = it.length; i < len; i++) {
            if (fn.call(scope || this, it[i], k[i])) {
                return i;
            }
        }
        return -1;
    },


    createValueMatcher: function (value, anyMatch, caseSensitive, exactMatch) {
        if (!value.exec) {
            var er = Ext.util.Format.escapeRegex;
            value = String(value);

            if (anyMatch === true) {
                value = er(value);
            } else {
                value = '^' + er(value);
                if (exactMatch === true) {
                    value += '$';
                }
            }
            value = new RegExp(value, caseSensitive ? '' : 'i');
        }
        return value;
    },


    clone: function () {
        var r = new Ext.util.MixedCollection();
        var k = this.keys, it = this.items;
        for (var i = 0, len = it.length; i < len; i++) {
            r.add(k[i], it[i]);
        }
        r.getKey = this.getKey;
        return r;
    }
});